using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;



namespace XmlObjects
{

  /// <summary>
  /// The Xml File Loader is used to load any XML file into a hierarchy of objects.
  /// The loader cannot be invoked simoultaneously from multiple threads. If an 
  ///    application requires Xml parsing from several threads, each thread must
  ///    create a separate XmlObjectsLoader instance.
  /// </summary>
  public class XmlObjectsManager
  {
    /// <summary>
    /// Simple constructor
    /// </summary>
    public XmlObjectsManager()
    {
      typeHelperHandler = new TypeHelperHandler();
    }



    /// <summary>
    /// Constructor that allows specifying the default values for the 
    ///   XmlTypeAttribute when the classes in the Xml hierarchy do not define them.
    /// For example, specifying the parentTextField as "parent" means that on every
    ///   referenced class with a "parent" field, it will be used to set its parent
    ///   in the hierarchy.
    /// Note the difference between the default values and setting explicitly the
    ///    attribute on a class: if explictly set, and the field does not exist, it 
    ///    is considered an error
    /// </summary>
    /// <param name="genericAttribute">generic attribute to use</param>
    public XmlObjectsManager(XmlTypeAttribute genericAtribute)
      : this()
    {
      typeHelperHandler.AttributeFields = genericAtribute.AttributeFields;
      typeHelperHandler.ExplicitFields = genericAtribute.ExplicitFields;
      typeHelperHandler.ProvidedFieldsSupport = genericAtribute.ProvidedFieldsSupport;
      typeHelperHandler.InnerTextField = genericAtribute.InnerTextField;
      typeHelperHandler.ParentField = genericAtribute.ParentField;
      typeHelperHandler.ChildsField = genericAtribute.ChildsField;
      typeHelperHandler.XmlWriteCompletedMethod = genericAtribute.XmlWriteCompletedMethod;
      typeHelperHandler.XmlWritePrepareMethod = genericAtribute.XmlWritePrepareMethod;
      typeHelperHandler.XmlReadCompletedMethod = genericAtribute.XmlReadCompletedMethod;
      typeHelperHandler.TypeAttribute = genericAtribute.TypeAttribute;
      typeHelperHandler.GenericElementField = genericAtribute.GenericElementField;
    }


    /// <summary>
    /// Basic Load method.
    /// It reads the passed input, and parses the root xml type into an 
    ///   instance of the rootType specified.
    /// </summary>
    /// <param name="rootType"></param>
    /// <param name="input"></param>
    /// <returns></returns>
    public object Load(Type rootType, TextReader input)
    {
      return Load(rootType, input, null, null);
    }



    /// <summary>
    /// It reads the passed input, and parses the root xml type into an 
    ///   instance of the rootType specified. 
    /// It is checked that the input's root matches the expected rootName
    /// </summary>
    /// <param name="rootType"></param>
    /// <param name="input"></param>
    /// <param name="rootName">Can be null, if no check is required</param>
    /// <returns></returns>
    public object Load(Type rootType, TextReader input, string rootName)
    {
      return Load(rootType, input, null, rootName);
    }



    /// <summary>
    /// It reads the passed input, validating it with the given schema, 
    ///   and parses the root xml type into an instance of the rootType 
    ///   specified. 
    /// </summary>
    /// <param name="rootType"></param>
    /// <param name="input"></param>
    /// <param name="schema">Can be null, if no validation is required</param>
    /// <returns></returns>
    public object Load(Type rootType, TextReader input, XmlSchema schema)
    {
      return Load(rootType, input, schema, null);
    }



    /// <summary>
    /// It reads the passed input, validating it with the given schema, 
    ///   and parses the root xml type into an instance of the rootType 
    ///   specified. 
    /// It is checked that the input's root matches the expected rootName
    /// </summary>
    /// <param name="rootType"></param>
    /// <param name="input"></param>
    /// <param name="schema">Can be null, if no validation is required</param>
    /// <param name="rootName">Can be null, if no check is required</param>
    /// <returns></returns>
    public object Load(Type rootType, TextReader input, XmlSchema schema,
        string rootName)
    {
      return Load(rootType, getReader(input, schema), rootName);
    }



    /// <summary>
    /// It parses the passed reader into an instance of the rootType specified. 
    /// It is checked as well that the input's root matches the expected rootName
    /// </summary>
    /// <param name="rootType"></param>
    /// <param name="reader"></param>
    /// <param name="rootName">Can be null, if no check is required</param>
    /// <returns></returns>
    public object Load(Type rootType, XmlReader reader, string rootName)
    {
      XmlInstanceCreator rootCreator = null;
      XmlInstanceCreator creator = null;
      while (reader.Read())
      {
        switch (reader.NodeType)
        {
          case XmlNodeType.Element:
            string name = reader.Name;
            bool endedElement = reader.IsEmptyElement;
            Dictionary<string, string> attributes = new Dictionary<string, string>();
            while (reader.MoveToNextAttribute())
            {
              attributes[reader.Name] = reader.Value;
            }
            if (creator == null)
            {
              if (rootCreator != null) //already created!
              {
                throw new XmlObjectsException(XmlObjectsError.MultipleRootsOnXmlDoc, name);
              }
              if ((rootName != null) && (rootName.ToLower() != name.ToLower()))
              {
                throw new XmlObjectsException(XmlObjectsError.UnexpectedXmlRoot, name, rootName);
              }
              creator = rootCreator = new XmlInstanceCreator(rootType, typeHelperHandler, attributes);
            }
            else
            {
              creator = creator.CreateElement(name, attributes, typeHelperHandler);
            }
            if (endedElement)
            {
              creator = creator.Completed();
            }
            break;


          case XmlNodeType.EndElement:

            creator = creator.Completed();
            break;


          case XmlNodeType.Text:
            creator.SetInnerText(reader.Value);
            break;


          default:
            break;
        }
      }

      if (creator != null) throw new XmlObjectsException(XmlObjectsError.UncompletedXmlDoc);
      if (rootCreator == null) return null;

      //ensure that the parent field in the rootCreator, if defined, is set to null
      rootCreator.SetXmlParent(null);
      rootCreator.BuildCompleted();
      return rootCreator.XmlElement;
    }



    /// <summary>
    /// Method to save an object as Xml output. The rootname is the
    ///   name (without namespace) of the type of the given object
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="output"></param>
    public void Save(object obj, XmlWriter output)
    {
      Save(obj, null, output);
    }



    /// <summary>
    /// Method to save an object as Xml output, using the given 
    ///   name as root name
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="rootName">Can be null, and then is used the
    ///   name of the type of obj</param>
    /// <param name="output"></param>
    public void Save(object obj, string rootName, XmlWriter output)
    {
      if (obj != null)
      {
        if (rootName == null) rootName = obj.GetType().Name;
        output.WriteStartElement(rootName);
        try
        {
          RecursiveSave(obj, obj.GetType(), output, new Hashtable());
        }
        catch (StackException sex)
        {
          output.Flush();
          throw sex.XmlObjectsException(obj, rootName);
        }
        output.WriteEndElement();
        output.Flush();
      }
    }

    /// <summary>
    /// Recursive method to store all the information from the given instance
    /// </summary>
    /// <param name="instance"></param>
    /// <param name="output"></param>
    /// <param name="declaredType">the type associated to this element. 
    /// Its real type must be the same or derived from it.</param>
    /// <param name="saveStack">The stack contains all the objects that are being
    ///    stored. That is, while subattributes and subelements of an object are
    ///    being treated, the stack keeps a reference to the objects being
    ///    handled, to detect recursive references.</param>
    private void RecursiveSave(object instance, Type declaredType,
      XmlWriter output, Hashtable saveStack)
    {
      //to handle the stack, the passed objects should not reimplement hash() equals()
      //in such a way that two different objects have same hash, same equal.
      //This should be the case, anyway. It could happen that two integers with the
      //same value (for example) are stored as elements; in this case, the stack would
      // consider both objects to be the same and would raise an exception. However,
      // the stack only contains the current recursive references, and an integer 
      // cannot contain the other integer.
      try
      {
        saveStack.Add(instance, instance);
      }
      catch (ArgumentException)
      {
        //if the element already exist, this exception is thrown
        throw new StackException(instance);
      }
      TypeHelper typeHelper = typeHelperHandler.CreateInstance(instance.GetType());

      //inform, if required, that Write is starting
      typeHelper.WriteStarted(instance);

      //write the attributes
      foreach (XmlAttribute attribute in typeHelper.GetAttributes(instance))
      {
        output.WriteAttributeString(attribute.Name, attribute.Value);
      }
      //write the type attribute, if needed
      XmlAttribute typeAttribute = typeHelper.GetTypeAttribute(declaredType, typeHelperHandler);
      if (typeAttribute!=null)
      {
        output.WriteAttributeString(typeAttribute.Name, typeAttribute.Value);
      }
      //then the inner text, if any
      output.WriteString(typeHelper.ConvertToString(instance));
      //and finally, the subelements, in a recursive way
      foreach (XmlElement element in typeHelper.GetElements(instance))
      {
        output.WriteStartElement(element.Name);
        try
        {
          RecursiveSave(element.Value, element.DeclaredType, output, saveStack);
        }
        catch (StackException sex)
        {
          sex.Extend(instance, element.Name);
          throw sex;
        }
        output.WriteEndElement();
      }

      //inform, if required, that Write is completed
      typeHelper.WriteCompleted(instance);

      saveStack.Remove(instance);
    }



    /// <summary>
    /// Private method to create a XmlReader from the input and the schema, 
    ///   which could be null
    /// This method could require further tuning, in case the user would like 
    ///   to analyse comments, etc.
    /// </summary>
    /// <param name="input"></param>
    /// <param name="schema">The schema to use, or null if no validation is required</param>
    /// <returns></returns>
    private XmlReader getReader(TextReader input, XmlSchema schema)
    {
      XmlReaderSettings settings = new XmlReaderSettings();
      settings.IgnoreComments = true;
      settings.IgnoreProcessingInstructions = true;
      settings.IgnoreWhitespace = true;
      if (schema == null)
      {
        settings.ValidationType = ValidationType.None;
      }
      else
      {
        settings.ValidationType = ValidationType.Schema;
        settings.Schemas.Add(schema);
      }
      return XmlReader.Create(input, settings);
    }



    /// <summary>
    /// Exception used to catch recursive problems while saving the xml
    /// </summary>
    private class StackException : Exception
    {
      public StackException(object cause)
      {
        this.cause = cause;
        source = new StringBuilder();
      }
      /// <summary>
      /// Returns the XmlObjectsException equivalent to the current StackException
      /// </summary>
      /// <param name="rootInstance"></param>
      /// <param name="rootName"></param>
      /// <returns></returns>
      public XmlObjectsException XmlObjectsException(object rootInstance, string rootName)
      {
        if (problem == null) problem = new StringBuilder();
        appendName(rootName);
        return new XmlObjectsException(XmlObjectsError.RecursiveReference,
          source.ToString(), problem.ToString());
      }

      /// <summary>
      /// Extends the exception with further stack information on the causing instance
      /// </summary>
      /// <param name="current">The current instance</param>
      /// <param name="name">The name of the element that is causing the exception.
      ///   (Name is not the name of the current instance)</param>
      public void Extend(object current, string name)
      {
        appendName(name);
        if (current == cause) problem = new StringBuilder();
      }

      /// <summary>
      /// Appends the name to the stack
      /// </summary>
      /// <param name="name"></param>
      private void appendName(string name)
      {
        if (problem != null) appendName(problem, name);
        appendName(source, name);
      }
      private void appendName(StringBuilder builder, string name)
      {
        if (builder.Length > 0) builder.Insert(0, ".");
        builder.Insert(0, name);
      }
      private object cause;
      private StringBuilder source, problem;
    }

    /// <summary>
    /// The typeHelperHandler must parse (using reflection) all the types related to the rootType
    /// If the parsing is done multiple times, keeping the information of the typeHelperHandler
    ///   can improve performance.
    /// </summary>
    private TypeHelperHandler typeHelperHandler;
  }
}
